package mosquito.g7;

import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.Comparator;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;

import org.apache.log4j.Logger;


public class Board {

    private Pixel[][] board;
    private Set<Line2D> walls; // walls are in internal representation

    private int internalWidth, internalHeight, width, height;
    private Point2D.Double[] pixels;

    private static final Logger logger = Logger.getLogger(Board.class);
	public enum Degree {
        N (90),
        NE (45),
        E (0),
        SE (-45),
        S (-90),
        SW (-135),
        W (180),
        NW (135);
        
        private final int degree;
        
        Degree(int degree) {
            this.degree = degree;
        }
        
        // will return in whatever representation it is given. does not convert
        public static Point2D.Double getPointDegreeAndDistanceFromPoint(Point2D.Double centerPoint, int degrees, double distance) {
            return new Point2D.Double(distance * Math.cos(-1*Math.toRadians(degrees)) + centerPoint.getX(), distance * Math.sin(-1*Math.toRadians(degrees)) + centerPoint.getY()); //TODO: EPIX HAX
        }
        
        public static double getDegreeDifferenceBetweenPoints(Point2D.Double p1, Point2D.Double p2) {
            return -1*Math.toDegrees(Math.atan2(p1.getY() - p2.getY(), p1.getX() - p2.getX()));
        }
        
        public int getDegree() { 
            return degree;
        }
    }

    
    public Board(Set<Line2D> walls) {
        this.internalWidth = (int) ((StaticZap.MAX_X - StaticZap.MIN_X) / StaticZap.GRANULARITY);
        this.internalHeight = (int) ((StaticZap.MAX_Y - StaticZap.MIN_Y) / StaticZap.GRANULARITY);
        this.width = StaticZap.MAX_X - StaticZap.MIN_X;
        this.height = StaticZap.MAX_Y - StaticZap.MIN_Y;

        board = new Pixel[internalWidth + (StaticZap.MIN_X == 0 ? 1 : 0)][internalHeight + (StaticZap.MIN_Y == 0 ? 1 : 0)];

        for(double x = StaticZap.MIN_X; x <= StaticZap.MAX_X; x += StaticZap.GRANULARITY) {
            for(double y = StaticZap.MIN_Y; y <= StaticZap.MAX_Y; y += StaticZap.GRANULARITY) {
                board[externalXtoInternalX(x)][externalYtoInternalY(y)] = new Pixel(x, y, externalXtoInternalX(x), externalYtoInternalY(y), this);
            }
        }
        
        // add edge boundaries in external format
        walls.add(new Line2D.Double(StaticZap.MIN_X, StaticZap.MIN_Y, StaticZap.MIN_X, StaticZap.MAX_Y));
        walls.add(new Line2D.Double(StaticZap.MIN_X, StaticZap.MIN_Y, StaticZap.MAX_X, StaticZap.MIN_Y));
        walls.add(new Line2D.Double(StaticZap.MAX_X, StaticZap.MIN_Y, StaticZap.MAX_X, StaticZap.MAX_Y));
        walls.add(new Line2D.Double(StaticZap.MIN_X, StaticZap.MAX_Y, StaticZap.MAX_X, StaticZap.MAX_Y));

        this.walls = new HashSet<Line2D>();
        //translate all walls to internal format
        for(Line2D wall : walls) {
            this.walls.add(new Line2D.Double(getPixel(wall.getP1()).getInternalPoint(), getPixel(wall.getP2()).getInternalPoint()));
        }

    }

    // x, y are abstracted, not the internal values
    public Pixel getPixel(double x, double y) {
        //do range checking
        if(x < StaticZap.MIN_X) x = StaticZap.MIN_X;
        else if(x > StaticZap.MAX_X) x = StaticZap.MAX_X;
        if(y < StaticZap.MIN_Y) y = StaticZap.MIN_Y;
        else if(y > StaticZap.MAX_Y) y = StaticZap.MAX_Y;
        
//        System.err.println(x + "\t" + y + "\t\t" + externalXtoInternalX(x) + "\t" + externalYtoInternalY(y) + "\t" + board[externalXtoInternalX(x)][externalYtoInternalY(y)]);
        return board[externalXtoInternalX(x)][externalYtoInternalY(y)];
    }
    
    // p is abstracted, not the internal values
    public Pixel getPixel(Point2D p) {
        return getPixel(p.getX(), p.getY());
    }

    private int externalXtoInternalX(double x) {
        return (int) Math.round(((x - StaticZap.MIN_X) / width) * internalWidth); //round deals with floating point errors
    }
    
    private int externalYtoInternalY(double y) {
        return (int) Math.round(((y - StaticZap.MIN_Y) / height) * internalHeight); //round deals with floating point errors
    }
    
    public Set<Line2D> getWalls() {
        return walls;
    }

    public Pixel getFurthestPixelAwayFromLinesAndEdgeBoundaries()
    {
        return getFurthestPixelAwayFromLinesAndBoundingBox(0, 0, internalWidth, internalHeight);
    }
    
    public PriorityQueue<Pixel> getFurthestPixelsAwayFromLinesAndEdgeBoundaries(int cuts)
    {
    	PixelSort pCompare = new PixelSort();
    	PriorityQueue<Pixel> pPixel = new PriorityQueue<Pixel>((cuts+1)*(cuts+1), pCompare);
    	
    	int cutSizeX = internalWidth/ (cuts+1);
    	int cutSizeY = internalHeight/ (cuts+1);
    	for(int i = 0; i < cuts+1; i++){
    		for(int j = 0; j < cuts+1; j++){
    			
    			Pixel p = getFurthestPixelAwayFromLinesAndBoundingBox(i*cutSizeX, j*cutSizeY, (i+1)*cutSizeX, (j+1)* cutSizeY);
    			logger.debug("Farthest point for section " + i + " " + j + " " + p.getExternalPoint().x + " , " + p.getExternalPoint().y);
    			pPixel.add(p);
    		}
    	}
    	

        return pPixel;
    }
    
    public Pixel getFurthestPixelAwayFromLinesAndBoundingBox(int x1, int y1, int x2, int y2)
    {
    	if(x1 > x2)
    		throw(new RuntimeException("X1 > X2!"));
    	if(y1 > y2)
        		throw(new RuntimeException("Y1 > Y2!"));
        int xBest = 0, yBest = 0;

        double distance = 0, minDistance = StaticZap.MAX_DISTANCE / StaticZap.GRANULARITY, tempDistance = StaticZap.MAX_DISTANCE / StaticZap.GRANULARITY;

        // find furthest point away
        for(int x = x1; x < x2; x++){
            for(int y = y1; y < y2; y++){
                minDistance = StaticZap.MAX_DISTANCE / StaticZap.GRANULARITY;
                for(Line2D wall : walls) {
                    tempDistance = wall.ptSegDist(x, y);
                    if(minDistance > tempDistance)
                        minDistance = tempDistance;
                }
                board[x][y].setDistanceFromAllWalls(minDistance);
                if(distance < minDistance) {
                    distance = minDistance;
                    xBest = x;
                    yBest = y;
                }

            }
        }
        return board[xBest][yBest];
    }
    
    public Point2D.Double getFurthestPointAwayFromLinesAndEdgeBoundaries() {
        return getFurthestPixelAwayFromLinesAndEdgeBoundaries().getExternalPoint();
    }
    
    public Point2D.Double[] getFurthestPointAwayFromLinesAndEdgeBoundaries(int slices, int maxLights) {
    	    
    	if(pixels == null) {
    	    pixels = new Point2D.Double[maxLights];
    	    PriorityQueue<Pixel> pPixel = getFurthestPixelsAwayFromLinesAndEdgeBoundaries(slices);
    	    for(int i=0; i < maxLights; i++)
    	        pixels[i] = pPixel.poll().getExternalPoint();    	    
    	}
    	
    	return pixels;
    }


    // point and radii is in external representation.
    public Pixel getPixelLeastInShadowWithinRadiiAndDegreeRangeFromPoint(double x, double y, double smallRadius, double largeRadius, int smallDegree, int largeDegree)
    {
        double leastPercentInShadow = 100; // max percentile
        Pixel bestPixel = getPixel(x, y);
        for (int theta = smallDegree; theta < largeDegree; theta++) {
            for(double radius = smallRadius; radius <= largeRadius; radius += StaticZap.GRANULARITY) {
                Pixel pixel = getPixel(Degree.getPointDegreeAndDistanceFromPoint(new Point2D.Double(x, y), theta, radius));
                if(pixel.shouldTryToPlaceLightHere()) {
                    if((pixel.getPercentInShadow() <= leastPercentInShadow && (theta - smallDegree) <= (largeDegree - smallDegree)/2)
                            || (pixel.getPercentInShadow() < leastPercentInShadow && (theta - smallDegree) > (largeDegree - smallDegree)/2)) {
                        Line2D.Double centerToRadii = new Line2D.Double(getPixel(x, y).getInternalPoint(), pixel.getInternalPoint());
                        boolean isVisible = true;
                        for(Line2D wall : pixel.getLinesWithinRadius()) {
                            if(wall.intersectsLine(centerToRadii)) {
                                isVisible = false;
                                break;
                            }
                        }
                        if(isVisible) {
                            leastPercentInShadow = pixel.getPercentInShadow();
                            bestPixel = pixel;                            
                        }
                    }    
                }
            }
        }
        if(bestPixel == getPixel(x, y)) // all pixels within the radii are on lines
            return null; //TODO: HANDLE THIS CASE PROPERLY!!!
        
        return bestPixel;

    }
    
    public Point2D.Double getPointLeastInShadowWithinRadiiAndDegreeRangeFromPoint(double x, double y, double smallRadius, double largeRadius, int smallDegree, int largeDegree)
    {
        Pixel pixel = getPixelLeastInShadowWithinRadiiAndDegreeRangeFromPoint(x, y, smallRadius, largeRadius, smallDegree, largeDegree);
        if(pixel == null)
            return null;
        return pixel.getExternalPoint();
    }
    
    public void clearNeverPlaceLightHereMarkers() {
        for(int i = 0; i < board.length; i++) {
            for(int j = 0; j < board[0].length; j++) {
                board[i][j].clearNeverPlaceLightHere();
            }
        }
    }

}

class PixelSort implements Comparator<Pixel>{



	@Override
	public int compare(Pixel o1, Pixel o2) {
		if(o1.getPercentInShadow() < o2.getPercentInShadow())
			return -1;
		if(o1.getPercentInShadow() > o2.getPercentInShadow())
			return 1;
		if(o1.getDistanceFromAllWalls() > o2.getDistanceFromAllWalls())
			return -1;
		if(o1.getDistanceFromAllWalls() < o2.getDistanceFromAllWalls())
			return 1;
		return 0;
	}
}
